import logging, md5, urllib, webbrowser, xml.dom.minidom as xdm
from restclient import GET, POST, fix_params

# module imports
from json import json_decode, json_encode
from utility import any, all
from error import FlickrError, FlickrFailure, CacheError
from authcache import AuthCache
from handler import Handler
from config import LOGGER_NAME

# URLs that we will use
FLICKR_HOST = 'http://api.flickr.com'
API_URL = FLICKR_HOST + '/services/'
DEFAULT_URLS = {
    'rest_url':API_URL + 'rest/',
    'auth_url':API_URL + 'auth/',
    'upload_url':API_URL + 'upload/',
    'replace_url':API_URL + 'replace/',
}

# prefixes that indicate an API call requires a POST, all the other ones
# are assumed to be GET
POST_PREFIXES = [
    'add',
    'create'
    'delete',
    'edit',
    'order',
    'post',
    'remove',
    'rotate',
    'set',
]

class Flickr(dict):
    """
    Python interface to the Flickr API

    To call an API function:
    
        flickr = Flickr(api_key=<api_key>, shared_secret=<shared_secret>)
        result = flickr.people.getInfo(user_id='79661467@N00')
    
    Or any other API method.  They are all handled as pretend attributes of the
    Flickr API object.  If an error occurs, you will be handed a FlickrError or
    subclass instance thereof.  This interface trys to contain a minimal
    knowledge of the values it is passing around, so if stuff breaks later,
    hopefully it's somewhere else.

    You may pass the name of a .ini file as the 'config' parameter to the
    constructor, values from the .ini file (in the [flickr] section) will be
    used as defaults.
        
        flickr = Flickr(config='flickr.ini')

    The result of an api call is parsed from the original JSON or XML response
    into a python dictionary, possibly with lists of python dictionaries inside
    of it.

    The attributes of the Flickr class are specially named to avoid any
    conflict with API methods, to clarify:
    
        flickr.<name>  is an api method
        flickr._<name> is a method used by the class itself
        flickr[<name>] is an attribute, for instance, the api_key is
                         stored as flickr['api_key']

    The Flickr API documentation is at:

        http://www.flickr.com/services/api/

    Be sure to check out the awesome API Explorer at the bottom of each Flickr
    method documentation page, e.g.:
    
        http://www.flickr.com/services/api/explore/?method=flickr.people.getInfo

    """
   
    def __init__(self, config=None, logging_level=logging.INFO, **kwargs):
        """
        Create a Flickr api object using the specified api_key and
        shared_secret. rest_url is the url to make the REST requests at,
        auth_url is the url to use in authentication, and both are, by default
        set to the normal Flickr urls.

        You may pass the name of a .ini file as the 'config' parameter, values
        from the .ini file (in the [flickr] section) will be used as defaults.

        Example .ini file:

            [flickr]
            api_key = 00000000000000000000000000000000
            shared_secret = 0000000000000000

        If there is a file at ~/.flickr/

        Get your Flickr API key at:
        
            http://www.flickr.com/services/api/keys/

        """
        
        self.log = logging.getLogger(LOGGER_NAME)
        self.log.setLevel(logging_level)

        self.update(DEFAULT_URLS)

        import os
        default_config = os.path.expanduser(os.path.sep.join(["~", ".flickr", "flickrrc.ini"]))
        from ConfigParser import NoSectionError
        try:
            self.update(self._loadConfig(default_config))
            self.log.debug('loaded default config')
        # if the file is not found or doesn't have the section, just keep going
        except NoSectionError:
            pass

        if config:
            self.update(self._loadConfig(config))
        self.update(kwargs)

        self['auth_cache'] = AuthCache()

    def _loadConfig(self, config_filename):
        from ConfigParser import ConfigParser
        config = ConfigParser()
        config.read(config_filename)
        return dict(config.items("flickr"))

    @property
    def logged_in(self):
        return self.has_key('auth')

    def login(self, perms='read', username=None, nsid=None,
              use_auth_cache=True, call_browser=None, waiting_f=None):
        """
        Attempt to login to Flickr.

        The .login() method throws FlickrError (or FlickrFailure) on failure.

        This function first searches the auth cache to see if there is already
        a cached token.  If more than one is found and the username or nsid is
        not specified, it will throw a CacheError.  If one is found, it is
        used.  A token will generally stay good until a user revokes privileges
        from your api key.

        If you don't want to use the auth cache, set use_auth_cache to false.

        If no tokens are found, the program must get one.  The normal way to do
        this for a regular app is to launch the browser asking the user to login
        to Flickr and permit your program.  If the user has already permitted
        your api key, he will see a screen indicating this.  Hopefully, after
        that the program will have a valid token that can be used for making
        privileged api calls. See:
        
            http://www.flickr.com/services/api/auth.howto.desktop.html

        Perms are in the list in ['read', 'write', 'delete'] and indicate how
        much permission the token you have grants you.

        username or nsid will tell the cache to look for a specific user.  If
        This is not passed, there must be only one user for the api key we have,
        because if there are multiple users stored in the cache, it won't be able
        to tell which one is desired.

        You may also pass two functions to this method, indicating how to call
        the browser and what to do to wait for the user.

        The default call_browser function is:

            def call_browser(auth_url):
                return webbrowser.open(auth_url)

        An example call_browser function is:
            
            def call_browser(auth_url):
                browser = 'firefox'
                os.spawnvp(os.P_WAIT, browser, [browser, auth_url])

        The default waiting_f is:
            
            def waiting_f():
                print "press a key once you have validated the frob"
                raw_input()

        More information:

            http://www.flickr.com/services/api/auth.spec.html
        """
        if self.logged_in:
            del self['auth']

        if not waiting_f:
            def waiting_f():
                print "press a key once you have validated the frob"
                raw_input()

        if not call_browser:
            call_browser = webbrowser.open

        # check to see if we have a token in the cache, return if we have one
        if use_auth_cache:
            if username:
                r = self.people.findByUsername(username=username)
                nsid = r['user']['nsid']

            try:
                # when passed the api object, .load_auth() will actually check all
                # the token before returning it
                auth = self['auth_cache'].load_auth(self['api_key'], nsid, perms, self)
            except CacheError:
                self.log.debug('failed to get auth info from cache')
                pass
            else:
                self.log.debug('got auth info from cache')
                self['auth'] = auth
                return

        # we don't have a valid token already, so we have to go get one
        result = self.auth.getFrob()
        self['frob'] = result['frob']
        self.log.debug('got frob:' + self['frob'])

        # we have an invalid frob, and we gotta validify it
        params = {
            'api_key': self['api_key'],
            'perms': perms,
            'frob': self['frob'],
        }
        params = self._sign(params)
        auth_url = self['auth_url'] + "?" + urllib.urlencode(params)

        self.log.debug('launching browser: ' + auth_url)
        call_browser(auth_url)
        # wait for the user to validate the frob, when this function
        # returns, we assume that it has been validated and try to
        # get a real token
        waiting_f()

        try:
            # I think the frob is good for something like an hour
            # if error.code == 108, that means the frob was invalid
            r = self.auth.getToken(frob=self['frob'])
        except FlickrError:
            raise
        else:
            self['auth'] = r['auth']
            if use_auth_cache:
                self['auth_cache'].cache_auth(self['api_key'], self['auth'])
            return

    def _get_methods(self):
        # used for the tab-autocompletion functions `trait_names`
        try:
            return self._methods
        except AttributeError:
            try:
                methods = self.reflection.getMethods()['methods']['method']
            except FlickrError:
                return []
            self._methods = [x.encode('utf8') for x in methods]
            return self._methods

    @property
    def __members__(self):
        # tab-completion
        methods = self._get_methods()
        methods = [x.replace('flickr.','') for x in methods]
        return methods

    def trait_names(self):
        # tab-completion for ipython
        return self.__members__

    def __getattr__(self, name):
        """
        Handle all the flickr.* api calls as pretend-attributes

        """
        self.log.debug('[flickr getattr] name: ' + name)

        if name[0] == '_':
            raise AttributeError, name

        return Handler(self, 'flickr.' + name)

    def _sign(self, params):
        """
        Sign query parameters using shared_secret, add signature to parameters.
        http://www.flickr.com/services/api/auth.spec.html

        """
        # fix_params can't be run on the same data twice
        fixed_params = fix_params(params.copy())
        s = self['shared_secret']
        for k, v in sorted(fixed_params.items()):
            if k == 'api_sig':
                continue
            # fix_params doesn't actually change all strings to utf8
            if isinstance(v, unicode):
                v = v.encode('utf8')
            s += k + v
        params['api_sig'] = md5.md5(s).hexdigest()
        return params

    def _rawcall(self, url, files={}, params={}):
        """
        Make a Flickr API call but do not parse the result.
        
        """
        # I like the JSON response a bit better, because it's not clear if
        # there will be multiple entries of tag in the XML format, so if 
        # there is only one result, it's not a list, but an item
        # with the JSON format, it's actually received as a list
#        p = {'api_key': self['api_key']} # without the json
        p = {'api_key': self['api_key'], 'format': 'json', 'nojsoncallback': 1}

        # see if we have a token to use with the call
        try:
            p['auth_token'] = self['auth']['token']
        except KeyError:
            pass

        p.update(params)
        p = self._sign(p)
        self.log.debug('url ' + url)
        self.log.debug('params ' + str(p))
        self.log.debug('files ' + str([(k,v['filename']) for k,v in files.iteritems()]))
       
        if p.has_key('method'):
            method_name = p['method'].split('.')[-1]
        else:
            method_name = ''

        if any(method_name.startswith(s) for s in POST_PREFIXES) or files:
            http_method = POST
        else:
            http_method = GET
        return http_method(url, params=p, files=files, async=False)

    def _call(self, *args, **kwargs):
        """
        Make a Flickr API call and return the parsed result. Or raise an error.

        """
        result = self._rawcall(*args, **kwargs)
        result = self._parse_response(result)
        self.log.debug('result ' + str(result))

        if result['stat'] == 'ok':
            return result
        elif result['stat'] == 'fail':
            raise FlickrFailure(result)
        else:
            raise FlickrError, "Aberrant response: " + str(result)

    def _parse_response(self, input):
        """
        Decide if the input is xml, if it's not, assume it is JSON.
        Some methods return xml even if asked for JSON.

        """
        if input[:5] == '<?xml':
            return self._parse_xml(input)
        else:
            return self._parse_json(input)

    def _parse_xml(self, input):
        """
        Ideally this converts the input into the same form as you would get
        from _parse_json, but in practice, it has no knowledge of multiple
        tags, so if there is only one of a tag, and there could be more, the
        data structure will not be a list.
        """
        d = xdm.parseString(input)
        response = self._convert_xml(d.childNodes[0])
        response = self._remove_content(response)
        return response

    def _parse_json(self, input):
        """
        Decode the JSON response into python objects.

        """
        response = json_decode(input)
        response = self._remove_content(response)
        return response

    def _remove_content(self, input):
        """
        When possible, remove '_content' keys.  That is, when there is a
        dictionary key that has a value that is a dictionary with only one key
        '_content':

            {key : {'_content' : value}}

        Set the value of the key to be the value of '_content':

            {key : value}

        """
        # This is probably not the greatest idea however, it is convenient.
        #
        # If this node has no attributes, just this content then making a whole
        # extra level is kinda redundant.
        #
        # But, of course, if they add some more properties to a key, besides
        # just the content, this will surely probably break something.

        if isinstance(input, dict):
            if '_content' in input.keys() and len(input.keys()) == 1:
                return input['_content']

            r = {}
            for k, v in input.iteritems():
                r[k] = self._remove_content(v)
            return r

        elif isinstance(input, list):
            for k, v in enumerate(input):
                input[k] = self._remove_content(v)
            return input

        else:
            return input

    def _convert_xml(self, node):
        """
        Parse the xml into python lists and dictionaries such that it
        resembles, as closely as possible, the result of a parsed JSON call.

        """
        r = {}
        if node.attributes:
            for k,v in node.attributes.items():
                r[k.encode('ascii')] = v
        if node.hasChildNodes():
            for child in node.childNodes:
                if child.nodeName == '#text':
                    # if the child's content is not just whitespace
                    if child.data.strip():
                        r['_content'] = child.data
                else:
                    key = child.nodeName
                    value = self._convert_xml(child)
                    if r.has_key(key):
                        if isinstance(r[key], list):
                            r[key].append(value)
                        else:
                            r[key] = [r[key]]
                    else:
                        r[key.encode('ascii')] = value
        if hasattr(node, 'data'):
            r['_content'] = node.data
        return r

    def _photo_operation(self, url, photo, **kwargs):
        """
        Performs either a photo replace or upload depending on the url.
        """
        files = {'photo':{'file':photo.read(), 'filename':photo.name},}
        return self._call(url=url, params=kwargs, files=files)

    def replace(self, photo, photo_id, **kwargs):
        """
        Replace an existing photo (identified by photo_id) with the given photo (a file object).

        http://www.flickr.com/services/api/replace.api.html
        """
        params = dict(photo_id=photo_id)
        params.update(kwargs)
        return self._photo_operation(self['replace_url'], photo, **params)

    def upload(self, photo, **kwargs):
        """
        Upload a photo (a file object).

        http://www.flickr.com/services/api/upload.api.html
        """
        return self._photo_operation(self['upload_url'], photo, **kwargs)
    
    def __repr__(self):
        return "<Flickr: api_key:%s>" % self['api_key']

